package org.test4j.mock.faking.modifier;

import g_asm.org.objectweb.asm.Label;
import g_asm.org.objectweb.asm.MethodVisitor;
import g_asm.org.objectweb.asm.Opcodes;
import g_asm.org.objectweb.asm.Type;
import org.test4j.mock.faking.FakeInvoker;
import org.test4j.mock.faking.util.TypeUtility;

import java.lang.reflect.Method;

import static g_asm.org.objectweb.asm.Opcodes.*;
import static g_asm.org.objectweb.asm.Type.VOID;
import static java.lang.reflect.Modifier.isStatic;
import static org.test4j.mock.faking.util.AsmConstant.api_code;
import static org.test4j.mock.faking.util.AsmType.*;
import static org.test4j.mock.faking.util.TypeDesc.*;
import static org.test4j.mock.faking.util.TypeUtility.isConstructor;

/**
 * 动态修改被mock方法实现
 *
 * @author darui.wu
 */
public class FakeMethodModifier extends MethodVisitor {
    private final FakeClassModifier cv;
    private final int access;
    private final String name;
    private final String desc;
    private final boolean isConstructor;

    FakeMethodModifier(FakeClassModifier cv, MethodVisitor mv, int access, String name, String desc) {
        super(api_code, mv);
        this.cv = cv;
        this.access = access;
        this.name = name;
        this.desc = desc;
        this.isConstructor = TypeUtility.isConstructor(name);
        if (!this.isConstructor) {
            this.callFakeMethod();
        }
    }

    private boolean alreadyCopied;

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        mv.visitMethodInsn(opcode, owner, name, desc, itf);
        if (this.fakeConstructor(owner, opcode, name)) {
            this.callFakeConstructor();
            alreadyCopied = true;
        }
    }

    /**
     * 紧跟在 this() or super()指令后面
     *
     * @param owner
     * @param opcode
     * @param name
     * @return
     */
    private boolean fakeConstructor(String owner, int opcode, String name) {
        if (!this.isConstructor || alreadyCopied) {
            return false;
        }
        if (!isConstructor(name) || opcode != INVOKESPECIAL) {
            return false;
        } else {
            // this(...) 或者 super(...)方法
            return owner.equals(this.cv.superClassName) || owner.equals(this.cv.classDesc);
        }
    }

    /**
     * For some reason, the start position for "this" gets displaced by byte code inserted at the beginning,
     * in a method modified by the EMMA tool. If not treated, this causes a ClassFormatError.
     */
    @Override
    public final void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
        if (end.getOffset() > 0) {
            Label _start = start.getOffset() > end.getOffset() ? end : start;
            mv.visitLocalVariable(name, desc, signature, _start, end, index);
        }
    }

    /**
     * 使用局部变量方式
     */
    public void callFakeMethod() {
        Type[] argTypes = Type.getArgumentTypes(this.desc);
        int index = this.argsObjectArray(argTypes);
        super.visitVarInsn(ASTORE, index + 1);
        this.byteCallFakeMethod(index + 1);

        super.visitVarInsn(ASTORE, index + 2);
        super.visitVarInsn(ALOAD, index + 2);

        //super.visitMethodInsn(Opcodes.INVOKESTATIC, T_String.PATH, "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false);
        this.visitLdcInsn(BridgeFakeInvocation.Enter_Non_Mock_Block);
        Label label = new Label();
        this.visitJumpInsn(IF_ACMPEQ, label);
        this.byteReturnValue(index + 2);
        this.visitLabel(label);
        // super.visitMaxs(0, 0);
    }

    /**
     * 不使用局部变量, 要不然和jacoco一起使用会报VerifyError
     */
    public void callFakeConstructor() {
        super.visitFieldInsn(GETSTATIC, FakeInvoker.getHostClassName(), BridgeFakeInvocation.BridgeID, T_InvocationHandler.DESC);
        /** 1st "invoke" arguments **/
        this.push1stArgThis();
        /** 2nd "invoke" arguments **/
        this.push2ndArgMethod();

        /** 3rd new Object[]{...}**/
        Type[] argTypes = Type.getArgumentTypes(this.desc);
        this.argsObjectArray(argTypes);
        //
        super.visitMethodInsn(INVOKEINTERFACE, T_InvocationHandler.PATH, "invoke", Invocation_Desc, true);
        this.visitLdcInsn(BridgeFakeInvocation.Enter_Non_Mock_Block);

        Label label = new Label();
        this.visitJumpInsn(IF_ACMPEQ, label);
        super.visitInsn(RETURN);
        this.visitLabel(label);
        // super.visitMaxs(0, 0);
    }


    private int argsObjectArray(Type[] argTypes) {
        /** 定义Object[]数组大小 **/
        super.visitIntInsn(BIPUSH, 3 + argTypes.length);
        super.visitTypeInsn(ANEWARRAY, T_Object.PATH);
        // Object[0]
        this.visitStringOfArray(0, cv.classDesc);
        // Object[1]
        this.visitStringOfArray(1, name);
        // Object[2]
        this.visitStringOfArray(2, desc);
        /** 将 args[] 追加到 Object[] 数组中 **/
        int localVarIndex = isStatic(this.access) ? 0 : 1;
        for (int index = 0; index < argTypes.length; index++) {
            Type type = argTypes[index];
            this.visitTypeOfArray(3 + index, type, localVarIndex);
            localVarIndex += argTypes[index].getSize();
        }
        return localVarIndex - (isStatic(this.access) ? 0 : 1);
    }

    private void byteCallFakeMethod(int localIndex) {
        super.visitFieldInsn(GETSTATIC, FakeInvoker.getHostClassName(), BridgeFakeInvocation.BridgeID, T_InvocationHandler.DESC);
        /** 1st "invoke" arguments **/
        this.push1stArgThis();
        /** 2nd "invoke" arguments **/
        this.push2ndArgMethod();
        /** 3rd arg: new Object[]{...} **/
        super.visitVarInsn(ALOAD, localIndex);
        super.visitMethodInsn(INVOKEINTERFACE, T_InvocationHandler.PATH, "invoke", Invocation_Desc, true);
    }

    private final void byteReturnValue(int localIndex) {
        Type returnType = Type.getReturnType(this.desc);
        if (returnType.getSort() == VOID) {
            super.visitInsn(returnType.getOpcode(IRETURN));
            return;
        }
        super.visitVarInsn(ALOAD, localIndex);
        this.visitTypeInsn(Opcodes.CHECKCAST, getTypeDesc(returnType));
        if (isPrimitive(returnType)) {
            String methodName = returnType.getClassName() + "Value";

            String typePath = PRIMITIVE_OBJECTS.get(returnType.getSort()).PATH;
            String methodDesc = "()" + returnType.getInternalName();
            super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, typePath, methodName, methodDesc, false);
        }
        super.visitInsn(returnType.getOpcode(IRETURN));
    }

    /**
     * 参数入栈: Object[index] = args[localVarIndex]
     *
     * @param index
     * @param type
     * @param localVarIndex
     */
    private void visitTypeOfArray(int index, Type type, int localVarIndex) {
        super.visitInsn(DUP);
        super.visitIntInsn(SIPUSH, index);
        super.visitVarInsn(type.getOpcode(ILOAD), localVarIndex);
        if (isPrimitive(type)) {
            String typePath = PRIMITIVE_OBJECTS.get(type.getSort()).PATH;
            String typeDesc = '(' + type.getDescriptor() + ")L" + typePath + ';';
            super.visitMethodInsn(Opcodes.INVOKESTATIC, typePath, "valueOf", typeDesc, false);
        }
        super.visitInsn(AASTORE);
    }

    /**
     * 压入 value 到 Object[index] 位置
     *
     * @param index
     * @param value
     */
    private final void visitStringOfArray(int index, String value) {
        super.visitInsn(DUP);
        super.visitIntInsn(SIPUSH, index);
        super.visitLdcInsn(value);
        super.visitInsn(AASTORE);
    }


    /**
     * 压入第二个变量 (Method)null
     */
    private void push2ndArgMethod() {
        super.visitInsn(ACONST_NULL);
        super.visitTypeInsn(CHECKCAST, T_Method.PATH);
    }

    /**
     * 压入第一个变量this或null(静态方法)
     */
    private void push1stArgThis() {
        if (isStatic(this.access)) {
            super.visitInsn(ACONST_NULL);
        } else {
            super.visitVarInsn(ALOAD, 0);
        }
    }

    /**
     * {@link java.lang.reflect.InvocationHandler#invoke(Object, Method, Object[])}
     */
    final static String Invocation_Desc = String.format("(%s%s[%s)%s", T_Object.DESC, T_Method.DESC, T_Object.DESC, T_Object.DESC);
}